/**
 * TAC AgentWare
 * http://www.sics.se/tac        tac-dev@sics.se
 *
 * Copyright (c) 2001-2005 SICS AB. All rights reserved.
 *
 * SICS grants you the right to use, modify, and redistribute this
 * software for noncommercial purposes, on the conditions that you:
 * (1) retain the original headers, including the copyright notice and
 * this text, (2) clearly document the difference between any derived
 * software and the original, and (3) acknowledge your use of this
 * software in pertaining publications and reports.  SICS provides
 * this software "as is", without any warranty of any kind.  IN NO
 * EVENT SHALL SICS BE LIABLE FOR ANY DIRECT, SPECIAL OR INDIRECT,
 * PUNITIVE, INCIDENTAL OR CONSEQUENTIAL LOSSES OR DAMAGES ARISING OUT
 * OF THE USE OF THE SOFTWARE.
 *
 * -----------------------------------------------------------------
 *
 * Author  : Joakim Eriksson, Niclas Finne, Sverker Janson
 * Created : 23 April, 2002
 * Updated : $Date: 2005/06/07 19:06:16 $
 *	     $Revision: 1.1 $
 * ---------------------------------------------------------
 * 
 * Important methods in TACAgent:
 *
 * Retrieving information about the current Game
 * ---------------------------------------------
 * int getGameID()
 *  - returns the id of current game or -1 if no game is currently plaing
 *
 * getServerTime()
 *  - returns the current server time in milliseconds
 *
 * getGameTime()
 *  - returns the time from start of game in milliseconds
 *
 * getGameTimeLeft()
 *  - returns the time left in the game in milliseconds
 *
 * getGameLength()
 *  - returns the game length in milliseconds
 *
 * int getAuctionNo()
 *  - returns the number of auctions in TAC
 *
 * int getClientPreference(int client, int type)
 *  - returns the clients preference for the specified type
 *   (types are TACAgent.{ARRIVAL, DEPARTURE, HOTEL_VALUE, E1, E2, E3}
 *
 * int getAuctionFor(int category, int type, int day)
 *  - returns the auction-id for the requested resource
 *   (categories are TACAgent.{CAT_FLIGHT, CAT_HOTEL, CAT_ENTERTAINMENT
 *    and types are TACAgent.TYPE_INFLIGHT, TACAgent.TYPE_OUTFLIGHT, etc)
 *
 * int getAuctionCategory(int auction)
 *  - returns the category for this auction (CAT_FLIGHT, CAT_HOTEL,
 *    CAT_ENTERTAINMENT)
 *
 * int getAuctionDay(int auction)
 *  - returns the day for this auction.
 *
 * int getAuctionType(int auction)
 *  - returns the type for this auction (TYPE_INFLIGHT, TYPE_OUTFLIGHT, etc).
 *
 * int getOwn(int auction)
 *  - returns the number of items that the agent own for this
 *    auction
 *
 * Submitting Bids
 * ---------------------------------------------
 * void submitBid(Bid)
 *  - submits a bid to the tac server
 *
 * void replaceBid(OldBid, Bid)
 *  - replaces the old bid (the current active bid) in the tac server
 *
 *   Bids have the following important methods:
 *    - create a bid with new Bid(AuctionID)
 *
 *   void addBidPoint(int quantity, float price)
 *    - adds a bid point in the bid
 *
 * Help methods for remembering what to buy for each auction:
 * ----------------------------------------------------------
 * int getAllocation(int auctionID)
 *   - returns the allocation set for this auction
 * void setAllocation(int auctionID, int quantity)
 *   - set the allocation for this auction
 *
 *
 * Callbacks from the TACAgent (caused via interaction with server)
 *
 * bidUpdated(Bid bid)
 *  - there are TACAgent have received an answer on a bid query/submission
 *   (new information about the bid is available)
 * bidRejected(Bid bid)
 *  - the bid has been rejected (reason is bid.getRejectReason())
 * bidError(Bid bid, int error)
 *  - the bid contained errors (error represent error status - commandStatus)
 *
 * quoteUpdated(Quote quote)
 *  - new information about the quotes on the auction (quote.getAuction())
 *    has arrived
 * quoteUpdated(int category)
 *  - new information about the quotes on all auctions for the auction
 *    category has arrived (quotes for a specific type of auctions are
 *    often requested at once).

 * auctionClosed(int auction)
 *  - the auction with id "auction" has closed
 *
 * transaction(Transaction transaction)
 *  - there has been a transaction
 *
 * gameStarted()
 *  - a TAC game has started, and all information about the
 *    game is available (preferences etc).
 *
 * gameStopped()
 *  - the current game has ended
 *
 */

package se.sics.tac.aw;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Logger;

import se.sics.tac.util.ArgEnumerator;

public class SimpleAgent extends AgentImpl {

	private static final Logger log = Logger.getLogger(SimpleAgent.class.getName());

	private static final boolean DEBUG = false;

	private static final float DEFAULT_HOTEL_PREMIUM_THRESHOLD = 75f;

	private static final float DEFAULT_ENTERTAINMENT_PREMIUM_THRESHOLD = 70f;
	
	private static final float DEFAULT_ENTERTAINMENT_INITIAL_BID_PRICE = 50f;

	private int totalGames;

	private float[] prices;

	private float[] maxHotelPrices;

	private boolean isHotelBidSent;

	private float[][] hotelPriceHistory = new float[2][4]; // 4 nights * 2 types of hotel

	private float[] maxEntertainmentPrice; // 3 types of entertainment

	protected void init(ArgEnumerator args) {
		prices = new float[agent.getAuctionNo()];
	}

	public void quoteUpdated(Quote quote) {
		int auction = quote.getAuction();
		int auctionCategory = agent.getAuctionCategory(auction);

		if (auctionCategory == TACAgent.CAT_ENTERTAINMENT) {
			int alloc = agent.getAllocation(auction) - agent.getOwn(auction);
			if (alloc != 0) {
				Bid bid = new Bid(auction);
				if (alloc < 0) {
					prices[auction] = 140f - (agent.getGameTime() * 120f) / 720000;

					if (prices[auction] < DEFAULT_ENTERTAINMENT_INITIAL_BID_PRICE) {
						prices[auction] = DEFAULT_ENTERTAINMENT_INITIAL_BID_PRICE;
					}
				} else {
					int auctionType = agent.getAuctionType(auction);

					if ((auctionType == TACAgent.TYPE_ALLIGATOR_WRESTLING)
					        && (prices[auction] == maxEntertainmentPrice[0])) {
						return;
					} else if ((auctionType == TACAgent.TYPE_AMUSEMENT)
					        && (prices[auction] == maxEntertainmentPrice[1])) {
						return;
					} else if ((auctionType == TACAgent.TYPE_MUSEUM) && (prices[auction] == maxEntertainmentPrice[2])) {
						return;
					}

					prices[auction] = 50f + (agent.getGameTime() * 100f) / 720000;

					if (auctionType == TACAgent.TYPE_ALLIGATOR_WRESTLING) {
						if (prices[auction] > maxEntertainmentPrice[0]) {
							prices[auction] = maxEntertainmentPrice[0];
						}
					} else if (auctionType == TACAgent.TYPE_AMUSEMENT) {
						if (prices[auction] > maxEntertainmentPrice[1]) {
							prices[auction] = maxEntertainmentPrice[1];
						}
					} else if (auctionType == TACAgent.TYPE_MUSEUM) {
						if (prices[auction] > maxEntertainmentPrice[2]) {
							prices[auction] = maxEntertainmentPrice[2];
						}
					}
				}

				bid.addBidPoint(alloc, prices[auction]);
				if (DEBUG) {
					log.finest("submitting bid with alloc=" + agent.getAllocation(auction) + " own="
					        + agent.getOwn(auction));
				}
				agent.submitBid(bid);
			}
		}
	}

	public void quoteUpdated(int auctionCategory) {
		log.fine("All quotes for " + agent.auctionCategoryToString(auctionCategory) + " has been updated");
		if (auctionCategory == TACAgent.CAT_FLIGHT && !isHotelBidSent) {
			calcMaxHotelPrices();
			sendHotelBids();
			isHotelBidSent = true;
		}
	}

	public void bidUpdated(Bid bid) {
		log.fine("Bid Updated: id=" + bid.getID() + " auction=" + bid.getAuction() + " state="
		        + bid.getProcessingStateAsString());
		log.fine("       Hash: " + bid.getBidHash());
	}

	public void bidRejected(Bid bid) {
		log.warning("Bid Rejected: " + bid.getID());
		log.warning("      Reason: " + bid.getRejectReason() + " (" + bid.getRejectReasonAsString() + ')');
	}

	public void bidError(Bid bid, int status) {
		log.warning("Bid Error in auction " + bid.getAuction() + ": " + status + " ("
		        + agent.commandStatusToString(status) + ')');
	}

	public void gameStarted() {
		log.fine("Game " + agent.getGameID() + " started!");

		prices = new float[agent.getAuctionNo()];
		maxEntertainmentPrice = new float[3];
		maxHotelPrices = new float[agent.getAuctionNo()];
		isHotelBidSent = false;

		calculateAllocation();
		sendBids();
	}

	public void gameStopped() {
		totalGames++;
		log.fine("Game Stopped!");
	}

	public void auctionClosed(int auction) {
		log.fine("*** Auction " + auction + " closed!");

		if (agent.getAuctionCategory(auction) == TACAgent.CAT_HOTEL) {
			int day = agent.getAuctionDay(auction) - 1;
			int type = agent.getAuctionType(auction) == TACAgent.TYPE_CHEAP_HOTEL ? 0 : 1;
			hotelPriceHistory[type][day] += agent.getQuote(auction).getAskPrice();
		}
	}

	private void sendBids() {
		for (int i = 0, n = agent.getAuctionNo(); i < n; i++) {
			int alloc = agent.getAllocation(i) - agent.getOwn(i);
			float price = -1f;
			switch (agent.getAuctionCategory(i)) {
			case TACAgent.CAT_FLIGHT:
				if (alloc > 0) {
					price = 1000; // Just buy it.
				}
				break;

			case TACAgent.CAT_ENTERTAINMENT:
				if (alloc < 0) {
					price = 200;
					prices[i] = 200f;
				} else if (alloc > 0) {
					price = DEFAULT_ENTERTAINMENT_INITIAL_BID_PRICE;
					prices[i] = DEFAULT_ENTERTAINMENT_INITIAL_BID_PRICE;
				}
				break;
			default:
				break;
			}

			if (price > 0) {
				Bid bid = new Bid(i);
				bid.addBidPoint(alloc, price);
				if (DEBUG) {
					log.fine("submitting bid with alloc=" + agent.getAllocation(i) + " own=" + agent.getOwn(i));
				}
				agent.submitBid(bid);
			}
		}
	}

	private void sendHotelBids() {
		for (int i = 0, n = agent.getAuctionNo(); i < n; i++) {
			int alloc = agent.getAllocation(i) - agent.getOwn(i);
			float price = -1f;

			if (agent.getAuctionCategory(i) == TACAgent.CAT_HOTEL) {
				if (alloc > 0) {
					price = maxHotelPrices[i];
					prices[i] = maxHotelPrices[i];
				} else {
					continue;
				}
			}

			if (price > 0) {
				Bid bid = new Bid(i);
				bid.addBidPoint(alloc, price);
				if (DEBUG) {
					log.fine("submitting bid with alloc=" + agent.getAllocation(i) + " own=" + agent.getOwn(i));
				}
				agent.submitBid(bid);
			}
		}
	}

	private int getAssignedHotelType(int client) {
		int hotelType;
		int hotelPremium = agent.getClientPreference(client, TACAgent.HOTEL_VALUE);

		if (totalGames > 0) {
			int inFlightIdx = agent.getClientPreference(client, TACAgent.ARRIVAL) - 1;
			int outFlightIdx = agent.getClientPreference(client, TACAgent.DEPARTURE) - 1;

			// Compare average hotel cost
			float cheapHotelCost = 0;
			float goodHotelCost = 0;
			for (int i = inFlightIdx; i < outFlightIdx; i++) {
				cheapHotelCost += hotelPriceHistory[0][i];
				goodHotelCost += hotelPriceHistory[1][i];
			}

			float difference = (goodHotelCost - cheapHotelCost) / totalGames;
			log.fine("Historical hotel type difference " + difference);
			if (difference > hotelPremium) {
				hotelType = TACAgent.TYPE_CHEAP_HOTEL;
			} else {
				hotelType = TACAgent.TYPE_GOOD_HOTEL;
			}
		} else { // No hotel price history
			if (hotelPremium >= DEFAULT_HOTEL_PREMIUM_THRESHOLD) {
				hotelType = TACAgent.TYPE_GOOD_HOTEL;
			} else {
				hotelType = TACAgent.TYPE_CHEAP_HOTEL;
			}
		}

		return hotelType;
	}

	private void calcMaxHotelPrices() {
		for (int i = 0; i < 8; i++) {
			int inFlight = agent.getClientPreference(i, TACAgent.ARRIVAL);
			int outFlight = agent.getClientPreference(i, TACAgent.DEPARTURE);
			int hotelPremium = agent.getClientPreference(i, TACAgent.HOTEL_VALUE);
			int auction;
			int hotelType = getAssignedHotelType(i);

			// Calculate the maximum prices we are willing to pay.
			int auctionFlightIn = agent.getAuctionFor(TACAgent.CAT_FLIGHT, TACAgent.TYPE_INFLIGHT, inFlight);
			int auctionFlightOut = agent.getAuctionFor(TACAgent.CAT_FLIGHT, TACAgent.TYPE_OUTFLIGHT, outFlight);

			float flightCost = agent.getQuote(auctionFlightIn).getAskPrice()
			        + agent.getQuote(auctionFlightOut).getAskPrice();
			log.fine("flightCost " + flightCost);

			float potentialEntLost = agent.getClientPreference(i, TACAgent.E1)
			        + agent.getClientPreference(i, TACAgent.E2) + agent.getClientPreference(i, TACAgent.E3);
			log.fine("potentialEntLost " + potentialEntLost);

			for (int d = inFlight; d < outFlight; d++) {
				auction = agent.getAuctionFor(TACAgent.CAT_HOTEL, hotelType, d);
				maxHotelPrices[auction] += flightCost + potentialEntLost
				        + (hotelType == TACAgent.TYPE_GOOD_HOTEL ? hotelPremium : 0);
			}
		}
	}

	private void calculateAllocation() {
		for (int i = 0; i < 8; i++) {
			int inFlight = agent.getClientPreference(i, TACAgent.ARRIVAL);
			int outFlight = agent.getClientPreference(i, TACAgent.DEPARTURE);
			int hotelType = getAssignedHotelType(i);

			// Get the flight preferences auction and remember that we are
			// going to buy tickets for these days. (inflight=1, outflight=0)
			int auction = agent.getAuctionFor(TACAgent.CAT_FLIGHT, TACAgent.TYPE_INFLIGHT, inFlight);
			agent.setAllocation(auction, agent.getAllocation(auction) + 1);
			auction = agent.getAuctionFor(TACAgent.CAT_FLIGHT, TACAgent.TYPE_OUTFLIGHT, outFlight);
			agent.setAllocation(auction, agent.getAllocation(auction) + 1);

			// allocate a hotel night for each day that the agent stays
			for (int d = inFlight; d < outFlight; d++) {
				auction = agent.getAuctionFor(TACAgent.CAT_HOTEL, hotelType, d);
				log.finer("Adding hotel for day: " + d + " on " + auction);
				agent.setAllocation(auction, agent.getAllocation(auction) + 1);
			}
		}

		calculateEntAllocation();
	}

	private void calculateEntAllocation() {
		List<EntertainmentInfo> entList = new ArrayList<EntertainmentInfo>();
		int[] recordCount = new int[3];

		for (int i = 0; i < 8; i++) {
			EntertainmentInfo entInfo;

			entInfo = new EntertainmentInfo();
			entInfo.setClient(i);
			entInfo.setEntertainmentType(TACAgent.TYPE_ALLIGATOR_WRESTLING);
			entInfo.setPremium(agent.getClientPreference(i, TACAgent.E1));
			if (entInfo.getPremium() >= DEFAULT_ENTERTAINMENT_PREMIUM_THRESHOLD) {
				entList.add(entInfo);
				recordCount[0]++;
				maxEntertainmentPrice[0] += entInfo.getPremium();
			}

			entInfo = new EntertainmentInfo();
			entInfo.setClient(i);
			entInfo.setEntertainmentType(TACAgent.TYPE_AMUSEMENT);
			entInfo.setPremium(agent.getClientPreference(i, TACAgent.E2));
			if (entInfo.getPremium() >= DEFAULT_ENTERTAINMENT_PREMIUM_THRESHOLD) {
				entList.add(entInfo);
				recordCount[1]++;
				maxEntertainmentPrice[1] += entInfo.getPremium();
			}

			entInfo = new EntertainmentInfo();
			entInfo.setClient(i);
			entInfo.setEntertainmentType(TACAgent.TYPE_MUSEUM);
			entInfo.setPremium(agent.getClientPreference(i, TACAgent.E3));
			if (entInfo.getPremium() >= DEFAULT_ENTERTAINMENT_PREMIUM_THRESHOLD) {
				entList.add(entInfo);
				recordCount[2]++;
				maxEntertainmentPrice[2] += entInfo.getPremium();
			}
		}

		// Calculate average premium
		for (int i = 0; i < 3; i++) {
			if (recordCount[i] > 0) {
				maxEntertainmentPrice[i] = maxEntertainmentPrice[i] / recordCount[i];
			}
		}

		EntertainmentComparator comparator = new EntertainmentComparator();
		Collections.sort(entList, comparator);

		int size = entList.size();
		for (int i = size - 1; i >= 0; i--) {
			EntertainmentInfo entInfo = entList.get(i);
			log.fine("Client " + entInfo.getClient() + " premium " + entInfo.getPremium());

			int auction = bestEntDay(agent.getClientPreference(entInfo.getClient(), TACAgent.ARRIVAL),
			        agent.getClientPreference(entInfo.getClient(), TACAgent.DEPARTURE), entInfo.getEntertainmentType());
			log.fine("Adding entertainment " + entInfo.getEntertainmentType() + " on " + auction);
			agent.setAllocation(auction, agent.getAllocation(auction) + 1);
		}
	}

	private int bestEntDay(int inFlight, int outFlight, int type) {
		int leastAllocatedAuction = agent.getAuctionFor(TACAgent.CAT_ENTERTAINMENT, type, inFlight); // Default value;
		int leastAllocatedNumber = Integer.MAX_VALUE; // Larger than the number of clients.

		for (int i = outFlight - 1; i >= inFlight; i--) {
			int auction = agent.getAuctionFor(TACAgent.CAT_ENTERTAINMENT, type, i);
			int allocation = agent.getAllocation(auction);

			if (allocation < agent.getOwn(auction)) {
				return auction;
			}

			if (allocation < leastAllocatedNumber) {
				leastAllocatedNumber = allocation;
				leastAllocatedAuction = auction;
			}
		}

		// If no left, take the one with least allocation
		return leastAllocatedAuction;
	}

	private class EntertainmentInfo {
		int client;
		int entertainmentType;
		int premium;

		public int getClient() {
			return client;
		}

		public void setClient(int client) {
			this.client = client;
		}

		public int getEntertainmentType() {
			return entertainmentType;
		}

		public void setEntertainmentType(int entertainmentType) {
			this.entertainmentType = entertainmentType;
		}

		public int getPremium() {
			return premium;
		}

		public void setPremium(int premium) {
			this.premium = premium;
		}
	}

	private class EntertainmentComparator implements Comparator {

		public int compare(Object arg0, Object arg1) {
			EntertainmentInfo ent0 = (EntertainmentInfo) arg0;
			EntertainmentInfo ent1 = (EntertainmentInfo) arg1;

			return ent0.getPremium() - ent1.getPremium();
		}
	}

	// -------------------------------------------------------------------
	// Only for backward compability
	// -------------------------------------------------------------------

	public static void main(String[] args) {
		TACAgent.main(args);
	}
} // SimpleAgent
